﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace LowLevelGraphics.Filter
{
    /// <summary>
    /// A Simple Spatial Tone Mapping Operator for High Dynamic Range Images
    /// K. K. Biswas, S. Pattanaik, School of Computer Science
    /// 
    /// NOTE: minimum image size is MEDIAN_RADIUS pixels
    /// NOTE: method presented in original paper uses 10x10 median
    /// </summary>
    public sealed class Biswas : Operator
    {
        private const int MEDIAN_RADIUS = 4;

        public double ContrastFactor
        {

            get
            {
                return this.contrast;
            }
            set
            {
                this.contrast = Math.Min(Math.Max(value, 0.15), 0.25);
            }
        }

        public bool Local
        {

            get
            {
                return this.local;
            }
            set
            {
                this.local = value;
            }
        }

        public double Saturation
        {

            get
            {
                return this.gamma;
            }
            set
            {
                this.gamma = Math.Min(Math.Max(value, 0.1), 1);
            }
        }

        private double contrast = 0.15;
        private bool local = true;
        private double gamma = 0.4;

        public Biswas(double contrastFactor, bool local, double saturation)
        {

            this.ContrastFactor = contrastFactor;
            this.Local = local;
            this.Saturation = saturation;
        }

        public Biswas()
        {
        }

        protected override void Run(float[][,] mapInput, int width, int height, ref float[][,] mapOutput)
        {

            int x = 0, y = 0;

            // compute luminance map and average luminance
            double lum = 0;
            double avgLum = 0;
            float[,] mapLum = new float[width, height];

            for (y = 0; y < height; y++)
                for (x = 0; x < width; x++)
                {

                    lum = (0.2992 * mapInput[2][x, y] + 0.5868 * mapInput[1][x, y] + 0.1140 * mapInput[0][x, y]);
                    avgLum += lum;
                    mapLum[x, y] = (float)lum;
                }

            avgLum /= (width * height);

            // map tones
            double gc = (this.contrast * avgLum);
            double yd = 0;

            if (this.local)
            {

                #region apply the local contrast factor

                double cl = 0;
                double yl = 0;

                for (y = 0; y < height; y++)
                    for (x = 0; x < width; x++)
                    {

                        lum = mapLum[x, y];
                        yl = GetMedian(mapLum, width, height, x, y);
                        cl = (yl * Math.Log(Double.Epsilon + yl / lum) + gc);
                        yd = (lum / (lum + cl));

                        mapOutput[2][x, y] = (float)Math.Min(Math.Max(yd * Math.Pow(mapInput[2][x, y] / lum, this.gamma), 0), 1);
                        mapOutput[1][x, y] = (float)Math.Min(Math.Max(yd * Math.Pow(mapInput[1][x, y] / lum, this.gamma), 0), 1);
                        mapOutput[0][x, y] = (float)Math.Min(Math.Max(yd * Math.Pow(mapInput[0][x, y] / lum, this.gamma), 0), 1);
                    }

                #endregion
            }
            else
            {

                #region apply the global contrast factor

                for (y = 0; y < height; y++)
                    for (x = 0; x < width; x++)
                    {

                        lum = mapLum[x, y];
                        yd = (lum / (lum + gc));

                        mapOutput[2][x, y] = (float)Math.Min(Math.Max(yd * Math.Pow(mapInput[2][x, y] / lum, this.gamma), 0), 1);
                        mapOutput[1][x, y] = (float)Math.Min(Math.Max(yd * Math.Pow(mapInput[1][x, y] / lum, this.gamma), 0), 1);
                        mapOutput[0][x, y] = (float)Math.Min(Math.Max(yd * Math.Pow(mapInput[0][x, y] / lum, this.gamma), 0), 1);
                    }

                #endregion
            }
        }

        protected override void SetParam(string param, string value)
        {

            switch (param)
            {
                case "contrastfactor":
                    this.ContrastFactor = Double.Parse(value);
                    break;
                case "local":
                    this.Local = Boolean.Parse(value);
                    break;
                case "saturation":
                    this.Saturation = Double.Parse(value);
                    break;
            }
        }

        private double GetMedian(float[,] mapLum, int width, int height, int x, int y)
        {

            int size = ((MEDIAN_RADIUS << 1) + 1);
            float[] values = new float[size * size];
            int index = 0;

            for (int fy = (y - MEDIAN_RADIUS); fy <= (y + MEDIAN_RADIUS); fy++)
                for (int fx = (x - MEDIAN_RADIUS); fx <= (x + MEDIAN_RADIUS); fx++)
                    values[index++] = mapLum[Clamp(fx, width), Clamp(fy, height)];

            Array.Sort(values);

            return values[values.Length >> 1];
        }

        private int Clamp(int x, int max)
        {

            if (x < 0)
                return (-x);

            if (x >= max)
                return ((max << 1) - 1 - x);

            return x;
        }
    }
}
